﻿package com.tomvian.maths
{
	import flash.display.DisplayObject;
	import flash.display.DisplayObjectContainer;
	import flash.display.Graphics;
	import flash.geom.Point;
	import flash.geom.Rectangle;

	/**
	 * Vector2D
	 * @author Thomas Vian
	 * @version 1.0
	 */
	final public class Vector2D
	{
		//--------------------------------------------------------------------------
		//
		//  Properties
		//
		//--------------------------------------------------------------------------
		
		public static const UP:Vector2D = new Vector2D(0, -1);
		public static const DOWN:Vector2D = new Vector2D(0, 1);
		public static const LEFT:Vector2D = new Vector2D(-1, 0);
		public static const RIGHT:Vector2D = new Vector2D(1, 0);
		public static const ORIGIN:Vector2D = new Vector2D(0, 0);
		
		public static var vectorPool:Vector.<Vector2D> = new Vector.<Vector2D>();
		
		public var x:Number;
		public var y:Number;
		
		//--------------------------------------------------------------------------
		//
		//  Getters / Setters
		//
		//--------------------------------------------------------------------------
		
		public function get length():Number {return Math.sqrt(x*x + y*y);}
		public function get lengthSqr():Number {return x*x + y*y;}
		public function get angle():Number {return Math.atan2(x, -y); /*getNew().angleTo(this);*/}
		
		//--------------------------------------------------------------------------
		//
		//  Constructor
		//
		//--------------------------------------------------------------------------
		
		function Vector2D(x:Number = 0.0, y:Number = 0.0)
		{
			this.x = x;
			this.y = y;
		}
		
		//--------------------------------------------------------------------------
		//
		//  Util Methods
		//
		//--------------------------------------------------------------------------
		
		public function toString():String
		{
			return "V2D[" + this.x + ", " + this.y + "]";
		}
		
		public function toPoint():Point
		{
			return new Point(x, y);
		}
		
		public function clone():Vector2D
		{
			return getNew(x, y);
		}
		
		public function mark(object:DisplayObject, colour:uint = 0, size:uint = 1):void
		{
			if(object["graphics"])
			{
				var graphics:Graphics = object["graphics"];
				graphics.lineStyle(0.01, colour);
				graphics.moveTo(x - size, y - size);
				graphics.lineTo(x + size, y + size);
				graphics.moveTo(x - size, y + size);
				graphics.lineTo(x + size, y - size);
			}
		}
		
		public function equals(vector:Vector2D, acceptRadius:Number = 0.0):Boolean
		{
			return Boolean(distance(this, vector) <= acceptRadius);
		}
		
		public function angleTo(vector:Vector2D):Number
		{
			var dx:Number = vector.x - x;
			var dy:Number = vector.y - y;
			
			return Math.atan2(dx, -dy);
		}
		
		public function dotProduct(vector:Vector2D):Number
		{
			return (x * vector.x) + (y * vector.y);
		}
		
		//--------------------------------------------------------------------------
		//
		//  Maths: Create New from Vector2D Methods
		//
		//--------------------------------------------------------------------------
		
		public function add(vector:Vector2D):Vector2D { 		return getNew(x + vector.x, y + vector.y); }
		public function subtract(vector:Vector2D):Vector2D { 	return getNew(x - vector.x, y - vector.y); }
		public function multiply(factor:Number):Vector2D { 		return getNew(x * factor, 	y * factor); }
		public function divide(factor:Number):Vector2D { 		return getNew(x / factor, 	y / factor); }
		
		public function rotate(angle:Number):Vector2D
		{
			var sin:Number = Math.sin(angle);
			var cos:Number = Math.cos(angle);
			return getNew(x*cos + y*sin, y*cos - x*sin);
		}
		
		public function rotateAbout(angle:Number, center:Vector2D = null):Vector2D
		{
			if (!center) center = ORIGIN;
			
			var temp:Vector2D = this.subtract(center);
			var sin:Number = Math.sin(angle);
			var cos:Number = Math.cos(angle);
			
			return getNew(temp.x * cos - temp.y * sin, temp.x * sin + temp.y * cos).addMod(center);
		}
		
		public function normalised(t:Number = 1.0):Vector2D
		{
			var t_p:Point = this.toPoint()
			t_p.normalize(t);
			return newFromPoint(t_p);
		}
		
		public function atLeast(length:Number):Vector2D
		{
			if(this.length >= length) 	return this.clone();
			else 						return this.normalised(length);
		}
		
		public function upTo(length:Number):Vector2D
		{
			if(this.length <= length) 	return this.clone();
			else 						return this.normalised(length);
		}
		
		public function normal(clockWise:Boolean = true):Vector2D
		{
			if(clockWise) 	return getNew(-y, x);
			else 			return getNew(y, -x);
		}
		
		public function reflect(normal:Vector2D):Vector2D
		{
			var l:Number = -this.dotProduct(normal) * 2.0;
			return getNew(x + normal.x * l, y + normal.y * l);
		}
		
		public function switchDO(from:DisplayObject, to:DisplayObject):Vector2D
		{
			var p:Point = this.toPoint();
			p = from.localToGlobal(p);
			p = to.globalToLocal(p);
			return newFromPoint(p);
		}
		
		public function inverseSquare():Vector2D
		{
			var l:Number = this.length;
			return this.normalised(1.0/(l*l));
		}
		
		public function interpolateTo(vector:Vector2D, t:Number):Vector2D
		{
			return getNew(x + (vector.x - x) * t, y + (vector.y - y) * t);
		}
		
		//--------------------------------------------------------------------------
		//
		//  Maths: Modify from Vector2D Methods
		//
		//--------------------------------------------------------------------------
		
		public function addMod(vector:Vector2D):Vector2D { 		this.x += vector.x; this.y += vector.y; return this; }
		public function subtractMod(vector:Vector2D):Vector2D { this.x -= vector.x; this.y -= vector.y; return this; }
		public function multiplyMod(factor:Number):Vector2D { 	this.x *= factor; 	this.y *= factor; 	return this; }
		public function divideMod(factor:Number):Vector2D { 	this.x /= factor; 	this.y /= factor; 	return this; }
		
		public function rotateMod(angle:Number):Vector2D
		{
			var sin:Number = Math.sin(angle);
			var cos:Number = Math.cos(angle);
			var x2:Number = x;
			x = (x * cos) - (y * sin);
			y = (x2 * sin) + (y * cos);
			return this;
		}
		
		public function rotateAboutMod(angle:Number, center:Vector2D = null):Vector2D
		{
			if (!center) center = ORIGIN;
			
			this.subtractMod(center);
			
			var sin:Number = Math.sin(angle);
			var cos:Number = Math.cos(angle);
			var tx:Number = this.x;
			
			this.x = this.x * cos - this.y * sin, 
			this.y = tx * sin + this.y * cos
			this.addMod(center);
			
			return this;
		}
		
		public function normalisedMod(t:Number = 1.0):Vector2D
		{
			var t_p:Point = this.toPoint()
			t_p.normalize(t);
			this.setFromPoint(t_p);
			return this;
		}
		
		public function atLeastMod(length:Number):Vector2D
		{
			if (this.length >= length) 	return this;
			else 						return this.normalisedMod(length);
		}
		
		public function upToMod(length:Number):Vector2D
		{
			if (this.length <= length) 	return this;
			else 						return this.normalisedMod(length);
		}
		
		public function normalMod(clockWise:Boolean = true):Vector2D
		{
			if(clockWise) 	return this.setAndReturn(-y, x);
			else 			return this.setAndReturn(y, -x);
		}
		
		public function reflectMod(normal:Vector2D):Vector2D
		{
			return this.addMod(normal.multiply(-2.0*this.dotProduct(normal)));
		}
		
		public function switchDOMod(from:DisplayObject, to:DisplayObject):Vector2D
		{
			var p:Point = this.toPoint();
			p = from.localToGlobal(p);
			p = to.globalToLocal(p);
			this.setFromPoint(p);
			return this;
		}
		
		public function inverseSquareMod():Vector2D
		{
			var l:Number = this.length;
			return this.normalisedMod(1.0/(l*l));
		}
		
		public function interpolateToMod(vector:Vector2D, t:Number):Vector2D
		{
			this.x = x + (vector.x - x) * t
			this.y = y + (vector.y - y) * t;
			return this;
		}
		
		//--------------------------------------------------------------------------
		//
		//  Maths: Create New from Numbers Methods
		//
		//--------------------------------------------------------------------------
		
		public function addNumbers		(dx:Number = 0.0, dy:Number = 0.0):Vector2D { return getNew(x + dx, y + dy); }
		public function subtractNumbers	(dx:Number = 0.0, dy:Number = 0.0):Vector2D { return getNew(x - dx, y - dy); }
		
		//--------------------------------------------------------------------------
		//
		//  Maths: Modify from Numbers Methods
		//
		//--------------------------------------------------------------------------
		
		public function addNumbersMod		(dx:Number = 0.0, dy:Number = 0.0):Vector2D { this.x += dx; this.y += dy; return this; }
		public function subtractNumbersMod	(dx:Number = 0.0, dy:Number = 0.0):Vector2D { this.x -= dx; this.y -= dy; return this; }
		
		//--------------------------------------------------------------------------
		//
		//  Setter Methods
		//
		//--------------------------------------------------------------------------
		
		public function setFromNumbers(x:Number = 0.0, y:Number = 0.0):void
		{
			this.x = x;
			this.y = y;
		}
		
		public function setFromAngle(angle:Number, length:Number = 1.0):void
		{
			x = Math.sin(angle) * length;
			y = -Math.cos(angle) * length;
		}
		
		public function setFromMouse(displayObject:DisplayObject):void
		{
			x = displayObject.mouseX
			y = displayObject.mouseY;
		}
		
		public function setFromPoint(point:Point):void
		{
			x = point.x;
			y = point.y;
		}
		
		public function copy(vector:Vector2D):void
		{
			x = vector.x;
			y = vector.y;
		}
		
		public function setFromDO(displayObject:DisplayObject):void
		{
			x = displayObject.x;
			y = displayObject.y;
		}
		
		public function setToDO(displayObject:DisplayObject):void
		{
			displayObject.x = x;
			displayObject.y = y;
		}
		
		public function moveBy(vector:Vector2D):void
		{
			x += vector.x;
			y += vector.y;
		}
		
		public function roundToNearest(nearest:Number = 0.01):void
		{
			x = Math.round(x / nearest) * nearest;
			y = Math.round(y / nearest) * nearest;
		}
		
		public function roundToPlaces(places:int = 2):void
		{
			this.roundToNearest(Math.pow(0.1, places));
			
			var xSplit:Array = String(x).split(".");
			var ySplit:Array = String(y).split(".");
			
			if (xSplit[1]) xSplit[1] = xSplit[1].substring(0, places);
			if (ySplit[1]) ySplit[1] = ySplit[1].substring(0, places);
			
			x = Number(xSplit.join("."));
			y = Number(ySplit.join("."));
		}
		
		private function setAndReturn(x:Number = 0.0, y:Number = 0.0):Vector2D
		{
			this.x = x;
			this.y = y;
			return this;
		}
		
		//--------------------------------------------------------------------------
		//
		//  Static Util Methods
		//
		//--------------------------------------------------------------------------
		
		public static function interpolate(vector1:Vector2D, vector2:Vector2D, t:Number):Vector2D
		{
			return getNew(vector1.x + (vector2.x - vector1.x) * t, vector1.y + (vector2.y - vector1.y) * t);
		}
		
		public static function distance(vector1:Vector2D, vector2:Vector2D):Number
		{
			var dx:Number = vector1.x - vector2.x;
			var dy:Number = vector1.y - vector2.y;
			return Math.sqrt(dx * dx + dy * dy);
		}
		
		public static function nearest(position:Vector2D, others:Array):Vector2D
		{
			var minDist:Number = Number.POSITIVE_INFINITY;
			var minID:int = 0;
			var gap:Number;
			
			for(var i:int = 0, l:int = others.length; i < l; i++)
			{
				gap = distance(position, others[i]);
				if(gap < minDist)
				{
					minDist = gap;
					minID = i;
				}
			}
			
			return others[minID];
		}
		
		//--------------------------------------------------------------------------
		//
		//  Static Creator Methods
		//
		//--------------------------------------------------------------------------
		
		public static function newFromPoint(point:Point):Vector2D
		{
			return getNew(point.x, point.y);
		}
		
		public static function newFromDisplayObject(displayObject:DisplayObject):Vector2D
		{
			return getNew(displayObject.x, displayObject.y);
		}
		
		public static function newFromMouse(displayObject:DisplayObject):Vector2D
		{
			return getNew(displayObject.mouseX, displayObject.mouseY);
		}
		
		public static function newFromAngle(angle:Number, length:Number = 1.0):Vector2D
		{
			return newFromPoint(Point.polar(length, angle - (Math.PI * 0.5)));
		}
		
		public static function newFromTrueCenter(displayObject:DisplayObject, context:DisplayObjectContainer):Vector2D
		{
			var rect:Rectangle = displayObject.getBounds(context);
			return new Vector2D(rect.x + rect.width * 0.5, rect.y + rect.height * 0.5);
		}
		
		public static function newFromRectCenter(rect:Rectangle):Vector2D
		{
			return new Vector2D(rect.x + rect.width * 0.5, rect.y + rect.height * 0.5);
		}
		
		//--------------------------------------------------------------------------
		//
		//  Pooling Methods
		//
		//--------------------------------------------------------------------------
		
		public static function addToPool(vectors:uint, limit:int = -1):void
		{
			var l:int = vectors;
			
			if (limit > 0)
			{
				var diff:int = limit - vectorPool.length;
				if (diff < vectors) l = diff;
			}
			
			for (var i:int = 0; i < l; i++) vectorPool.push(new Vector2D());
		}
		
		public static function fillPool(vectors:uint):void
		{
			for (var i:int = 0, l:int = vectors - vectorPool.length; i < l; i++) vectorPool.push(new Vector2D());
		}
		
		private static function getNew(x:Number = 0.0, y:Number = 0.0):Vector2D
		{
			if (vectorPool.length > 0.0) return vectorPool.pop().setAndReturn(x, y);
			return new Vector2D(x, y);
		}
	}
}